﻿ /*
  * STemplate
  * 
  * Licensed under the Apache License, Version 2.0 (the "License");
  * You may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *		http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  *
  */
  
STemplate = {};

STemplate.defaultSet = {
	version: '1.0.0',
	syntaxTable : {
		'if'	: { endwith: '/if', prefix: 'if (', suffix: ') {', paramMin: 1 },
		
		'elif'	: { prefix: '} else if (', suffix: ') {', paramMin: 0, paramDefault: 'true' },
		
		'/if'	: { isEnd: true, prefix: '}' },
		
		'loop'	: { endwith: '/loop', paramMin: 1,
					localParam: true, 
					prefixFunc: function (paramStr, state) {
						var _params = paramStr.match(/^([\w.]+)(?:\s+as\s+([\w.]+)(?:\s*=>\s*([\w.]+))?)?$/i);
						if (null === _params) {
							state.error(paramStr);
							return false;	
						}
						var fname = _params[1].replace(/\./g, '_'); 												
						var listVar = '__LIST__' + fname;
						var iterVar = _params[3] ? _params[3] : 
								    (_params[2] ? _params[2] : (fname + '_ivalue'));
						var proVar = _params[3] ? _params[2] : (fname + '_ipro');
						var countVar = fname + '_ct';
						return ['var ', listVar, ' = typeof ', _params[1], ' ==\'undefined\' ? false : ', _params[1], ';',
								'var ', countVar, ' = 0; \nvar __LOOP_COUNT__ = 0;\n', 
								'if (', listVar, ') {\n',
									'var ', iterVar, ';\n',
									'for (var ', proVar, ' in ', listVar, ') {\n',
										'if (typeof ', listVar, '[', proVar, "] == 'function') continue;\n",
										countVar, '++; __LOOP_COUNT__ ++;\n', 
										iterVar, ' = ', listVar, '[', proVar, '];\n'].join('');
						} },
						
		'eloop'	: { paramMin: 0, paramDefault: 'true', prefix: '\n} \n}\n if (__LOOP_COUNT__ == 0) {\n if (', suffix: ') {\n' },
		
		'/loop'	: { isEnd: true, prefix: '\n}\n }\n' },
		
		'var'	: { prefix: 'var ', suffix: ';\n' },
		
		'macro'	: { endwith: '/macro',
					localParam:true,
					prefixFunc: function (paramStr, state) {
						if (!/^\w+\s*\((\s*\w+\s*,)*(\s*\w+\s*)?\)$/.test(paramStr)) {
							state.error(paramStr);
							return false;	
						}
						return ['var ', paramStr.replace(/\(/, '= function ('),
						'\n{var _out_arr = [];\n var _out = {write: function (m) { if (m) _out_arr.push(m); } };\n'].join('');
					} },
					
		'/macro': { isEnd: true, prefix: " return _out_arr.join(''); };\n" },
		
		'code'	: { isBlock: true, endwith: '/code', dealFunc: function (blockText, out, state) { addLineNum(blockText, state); out.push('(function(){' + blockText + '})();\n'); } },
		
		'data'	: { isBlock: true, endwith: '/data', dealFunc: function (blockText, out, state) { dealText(blockText, out, state); } }
	},
	
	ldelim: '{',
	
	rdelim: '}',
	
	expPrefix: '$',
	
	modifiers: {
		'default' : function (o, n) { return o || n; }
	}	
};
(function () {
	if (!String.prototype.STrim) {
		String.prototype.STrim = function () {
			return this.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');	
		};	
	}
	
	STemplate.tEval = function (s) { 
		var ret = null;
		try {
			ret = eval(s); 
		} catch (e) {
			throw "STemplate.tEval:" + e + ";code:" + s;	
		}
		return ret;
	};

	STemplate.parse = function (tplName, content, sets) {
		sets = sets || {};
		sets.syntaxTable = sets.syntaxTable || STemplate.defaultSet.syntaxTable;
		sets.modifiers = sets.modifiers || {};
		for (var pro in STemplate.defaultSet.modifiers) {
			sets.modifiers[pro] = sets.modifiers[pro] || STemplate.defaultSet.modifiers[pro];	
		}
		sets.ldelim = sets.ldelim || STemplate.defaultSet.ldelim;
		sets.rdelim = sets.rdelim || STemplate.defaultSet.rdelim;
		sets.expPrefix = sets.expPrefix || STemplate.defaultSet.expPrefix;
		var result = parse(tplName, content, sets);
		if (result.state.exceptions.length) {
			throw result.state.exceptions.join('\n');	
		}
		var func = STemplate.tEval(result.funcStr);
		if (func) return new STemplate.Template(tplName, func, result.funcStr, sets);
		return null;
	};
	
	var parse = function (tplName, content, sets) {
		var content = format(content);
		var state = { error: function (str) { this.exceptions.push(this.tplName + ' syntax error at line:' + this.line + ' ' + str ); }, tplName: tplName, exceptions: [], stack: [], line: 1 };
		var out = ['var STemplate_template_function = function (_out, _context) { var out = _context; with (_context) {'];
		var curIndex = 0;
		while (curIndex < content.length) {
			var ldelimIndex = content.indexOf(sets.ldelim, curIndex);
			if (ldelimIndex == -1) {
				break;	
			}
			//忽略转义的界定符
			if (content.charAt(ldelimIndex - 1) == '\\') {
				dealText(content.substring(curIndex, ldelimIndex + 1), out, state);
				curIndex = ldelimIndex + 1;
				continue;
			}
			
			var rdelimIndex = content.indexOf(sets.rdelim, ldelimIndex);
			if (rdelimIndex == -1) {
				state.error('missing right delimter \'' + sets.rdelim);
				break;	
			}
			
			//表达式处理
			if (content.charAt(ldelimIndex - 1) == sets.expPrefix) {	
				dealText(content.substring(curIndex, ldelimIndex - 1), out, state);
				curIndex = ldelimIndex - 1;
				if (dealExpression(content.substring(ldelimIndex + 1, rdelimIndex), out, state)) {
					curIndex = rdelimIndex + 1;	
					continue;
				} else {
					break;
				}
			}
			
			dealText(content.substring(curIndex, ldelimIndex), out, state);
			curIndex = ldelimIndex;
				
			//其它语句处理
			var staObj = getStatementObj(content.substring(ldelimIndex + 1, rdelimIndex), sets);
			if (null === staObj || null === staObj.keywordObj) {
				state.error('void statement' + staObj.statement);
				break;
			}
			if (staObj.keywordObj.paramMin && (staObj.paramStr == null || staObj.paramStr.split(' ').length < staObj.keywordObj.paramMin)) {
				state.error('missing params ' + staObj.statement);
				break;	
			};
			
			var ko = staObj.keywordObj;
			//块语句处理
			if (ko.isBlock) {
				var closeTagIndex = content.indexOf(sets.ldelim + ko.endwith + sets.rdelim, rdelimIndex);
				if (closeTagIndex == -1) {
					state.error('missing close tag \'' + ko.endwith + '\'');
					break;	
				}
				var blockText = content.substring(rdelimIndex + 1, closeTagIndex);
				ko.dealFunc.apply(this, [blockText, out, state]);
				curIndex = closeTagIndex + (sets.ldelim + ko.endwith + sets.rdelim).length;
				continue;
			}
			if (!dealStatement(staObj, out, state)) break;
			curIndex = rdelimIndex + 1;
		}
		dealText (content.substring(curIndex), out, state);
		out.push('}}; STemplate_template_function');
		return { 'state': state, 'funcStr': out.join('') };
	};
	
	var getStatementObj = function (statement, sets) {
		var ma = statement.STrim().match(/^(.+?)(?:\s+(.+?))?$/);
		if (null === ma) return null;
		return { keywordObj: sets.syntaxTable[ma[1]] || null, keyword: ma[1], paramStr: ma[2] || null, statement: statement };	
	};
	
	var dealStatement = function (staObj, out, state) {
		var ko = staObj.keywordObj;
		if (ko.endwith) {
			state.stack.push(ko.endwith);	
		}
		//[].pop() == undefined
		if (ko.isEnd && staObj.keyword != state.stack.pop()) {
			state.error('close tag not match!');
			return false;	
		}
		var paramStr = ko.localParam ? '' : (staObj.paramStr || ko.paramDefault || '');
		var prefix = ko.prefixFunc ? ko.prefixFunc(staObj.paramStr, state) : (ko.prefix || '');
		var suffix = ko.suffix || '';
		out.push(prefix + paramStr + suffix);
		return true;
	};
	
	 var dealExpression = function (content, out, state) {
		//a|default b|modifer c=> _context['modifer'](_context['default'](a, b), c);
		var parts = content.STrim().replace(/\|\|/g, '_##_').split('|');
		if (null === parts) return true;
		var str = parts[0];
		for (var i = 1; i < parts.length; i ++) {
			var part = parts[i].replace(/_##_/g, '||').STrim();
			str = part.replace(/^(\S+)(\s|$)/, '_context[\'$1\'](' + str + ',').replace(/(,?$)/, ')');		
		}
		str = str.replace(/_##_/g, '||');
		out.push('_out.write(' + str + ');');
		return true;
	};
	
	this.dealText = function (content, out, state) {
		addLineNum(content, state);
		content = content.replace(/\\(\{|\})/g, '$1').replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
		out.push('_out.write("' + content + '");');
	};
	
	this.addLineNum = function (content, state) {
		var i = -1;
		do {
			i = content.indexOf('\n', i + 1);
			if (i >= 0) state.line ++;
		} while (i > 0);
	};
	
	var format = function (content) {
		return content.replace(/\t/g, '    ').replace(/\r\n/g, '\n').replace(/\r/g, '\n');	
	};
	
	STemplate.Template = function (tplName, func, funcStr, sets) {
		this.name = tplName;
		this.source = funcStr;
		this.process = function (context) {
			context = context || {};
			for (var pro in sets.modifiers) {
				context[pro]	= context[pro] || sets.modifiers[pro];
			}
			var outArr = [];
			var out = { write: function (s) { outArr.push(s); } };
			func(out, context);
			return outArr.join('');
		};
	};
})();